/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.Collections.ObjectModel;

namespace DaveSexton.DocProject.DocSites
{
	public delegate QueryOperator QueryOperatorFeedback(string word);

	/// <remarks>
	/// This class is not thread-safe.
	/// </remarks>
	public sealed class QueryExpressionFactory
	{
		#region Public Properties
		public int MinimumKeywordLength
		{
			get
			{
				return minimumKeywordLength;
			}
			set
			{
				if (value < 1)
					throw new ArgumentOutOfRangeException("value", value, Resources.Errors.ValueMustBeGreaterThan1);

				minimumKeywordLength = value;
			}
		}

		public IEnumerable<string> IgnoredWords
		{
			get
			{
				return new ReadOnlyCollection<string>(ignoredWords);
			}
			set
			{
				ignoredWords.Clear();

				if (value != null)
					ignoredWords.AddRange(ListHelper.CleanAndSort(value,
						CleanListOptions.RemoveDuplicates | CleanListOptions.Trim, StringComparer.CurrentCultureIgnoreCase));
			}
		}

		public IEnumerable<QueryOperator> UnaryQueryOperators
		{
			get
			{
				return new ReadOnlyCollection<QueryOperator>(unaryQueryOperators);
			}
			set
			{
				unaryQueryOperators.Clear();

				if (value != null)
					unaryQueryOperators.AddRange(value);
			}
		}

		public string SplitQueryCharacters
		{
			get
			{
				return splitQueryCharacters;
			}
			set
			{
				splitQueryCharacters = value ?? string.Empty;
			}
		}

		public QueryOperatorFeedback QueryOperatorFeedback
		{
			get
			{
				return queryOperatorFeedback;
			}
			set
			{
				queryOperatorFeedback = value;
			}
		}

		public QueryOperator DefaultOperator
		{
			get
			{
				return defaultOperator;
			}
			set
			{
				defaultOperator = value;
			}
		}

		public bool Optimize
		{
			get
			{
				return optimize;
			}
			set
			{
				optimize = value;
			}
		}
		#endregion

		#region Private / Protected
		private List<string> parsedWords;
		private QueryExpression result;
		private string symbolsBeforeTerm, splitQueryCharacters = string.Empty;
		private bool optimize = true;
		private QueryOperator defaultOperator = '&';
		private QueryOperatorFeedback queryOperatorFeedback;
		private int minimumKeywordLength;

		private readonly List<QueryOperator> unaryQueryOperators = new List<QueryOperator>();
		private readonly List<string> ignoredWords = new List<string>();
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="QueryExpressionFactory" /> class.
		/// </summary>
		public QueryExpressionFactory()
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="QueryExpressionFactory" /> class.
		/// </summary>
		private QueryExpressionFactory(QueryExpressionFactory cloneForSplitWords)
		{
			// NOTE: Since this constructor is only used for creating an instance to evaluate split words
			// from another instance of this class, certain properties do not have to be copied since they've
			// already been evaluated on the terms before they were split.  Also, the lists of operators
			// should not be copied (other than the defaultOperator) since split words should not be treated
			// as operators. e.g., where "and" is the '&' operator and the factory splits on '.', then 
			// "this.and.that" should return {this & and & that}, not {this & that}.  Ignored words do not
			// have to be copied since the words that will be passed to the CreateExpression method on this
			// instance will have already been evaluated against the IgnoredWords list.
			this.minimumKeywordLength = cloneForSplitWords.minimumKeywordLength;
			this.optimize = cloneForSplitWords.optimize;
			this.defaultOperator = cloneForSplitWords.defaultOperator;
		}
		#endregion

		#region Methods
		private QueryExpression ParseWord(string word)
		{
			if (word.Length == 0)
				return result;

			System.Diagnostics.Debug.WriteLine("Parsing word: \"" + word + "\"");

			QueryExpression expr = DoOperatorFeedback(word);

			if (expr != null)
				return expr;
			else if (word.Length < minimumKeywordLength || ShouldIgnoreWord(word))
				return result;
			else if (splitQueryCharacters != null)
				return JoinExpressions(result, SplitWord(word));
			else
				return JoinExpressions(result, new TermQueryExpression(word));
		}

		private QueryExpression SplitWord(string word)
		{
			List<string> splitWords = new List<string>(word.Length / (minimumKeywordLength + 1));
			splitWords.Add(null);		// the code below works with indices so at least one is required by default

			for (int currentIndex = 0, position = 0; position < word.Length; position++)
			{
				char c = word[position];

				if (char.IsLetterOrDigit(c))
					splitWords[currentIndex] += c;
				else if (splitQueryCharacters.IndexOf(c) > -1)
				{
					string current = splitWords[currentIndex];

					if (current == null || current.Length < minimumKeywordLength || ShouldIgnoreWord(current))
						splitWords[currentIndex] = null;
					else
					{
						parsedWords.Add(current);
						splitWords.Add(null);
						currentIndex++;
					}
				}
			}

			if (splitWords.Count == 1)
			{
				string term = splitWords[0];

				if (term == null)
					return null;
				else
					return new TermQueryExpression(term);
			}
			else
			{
				// NOTE: Separate terms by spaces so that the default operator is used by the factor
				string terms = string.Join(" ", splitWords.ToArray());

				if (terms.Length <= splitWords.Count)
					return null;

				QueryExpressionFactory factory = new QueryExpressionFactory(this);
				return factory.CreateExpression(terms);
			}
		}

		private QueryExpression DoOperatorFeedback(string word)
		{
			if (queryOperatorFeedback != null)
			{
				QueryOperator op = queryOperatorFeedback(word);

				if (op != QueryOperator.None)
				{
					if (result != null)
						return new BinaryQueryOperation(result, op);
					
					// ignore an operator if there is no left-hand operand
				}
			}

			return null;
		}

		private void WriteSymbolPrefix(StringBuilder word)
		{
			if (symbolsBeforeTerm != null)
			{
				string symbols = symbolsBeforeTerm;
				symbolsBeforeTerm = null;

				if (symbols.Length == 1)
				{
					char symbol = symbols[0];

					if (unaryQueryOperators != null && unaryQueryOperators.Contains(symbol))
					{
						result = JoinExpressions(result, new UnaryQueryOperation(new QueryOperator(symbol)));
						return;
					}
				}

				word.Append(symbols);
			}
		}

		private bool ShouldIgnoreWord(string word)
		{
			return ignoredWords.BinarySearch(word, StringComparer.CurrentCultureIgnoreCase) >= 0 || parsedWords.Contains(word);
		}

		private QueryExpression JoinExpressions(QueryExpression expr1, QueryExpression expr2)
		{
			if (expr1 == null)
				return expr2;
			else if (expr2 == null)
				return expr1;

			QueryOperation operation = expr1 as QueryOperation;

			if (operation != null && operation.MissingOperand)
			{
				operation.AppendOperand(expr2);
				return operation;
			}
			else
				return new BinaryQueryOperation(expr1, defaultOperator, expr2);
		}

		public QueryExpression CreateExpression(string text)
		{
			if (text == null)
				throw new ArgumentNullException("text");

			parsedWords = new List<string>(text.Length / (minimumKeywordLength + 1));
			symbolsBeforeTerm = null;
			Stack<QueryExpression> groupStack = new Stack<QueryExpression>(8);

			StringBuilder word = new StringBuilder(text.Length);

			for (int i = 0; i < text.Length; i++)
			{
				char c = text[i];

				if (!char.IsWhiteSpace(c))
				{
					if (c == '(')
					{
						WriteSymbolPrefix(word);

						if (word.Length > 0)
						{
							result = ParseWord(word.ToString());

							word = new StringBuilder(text.Length - i - 1);
						}

						groupStack.Push(result);
						result = null;
						symbolsBeforeTerm = null;
					}
					else if (c == ')')
					{
						if (groupStack.Count > 0)
						{
							if (word.Length > 0)
							{
								result = ParseWord(word.ToString());

								word = new StringBuilder(text.Length - i - 1);
							}

							result = JoinExpressions(groupStack.Pop(), result);
						}
					}
					else
					{
						bool isWord = char.IsLetterOrDigit(c) || c == '\'';

						if (!isWord && word.Length == 0)
							symbolsBeforeTerm += c;
						else
						{
							WriteSymbolPrefix(word);

							word.Append(c);
						}
					}
				}
				else if (word.Length > 0)
				{
					result = ParseWord(word.ToString());

					word = new StringBuilder(text.Length - i - 1);
				}
				else if (symbolsBeforeTerm != null)
				// one or more floating symbols (not near any letters or digits)
				{
					if (queryOperatorFeedback != null)
					{
						QueryExpression expr = DoOperatorFeedback(symbolsBeforeTerm);

						if (expr != null)
							result = expr;
					}

					symbolsBeforeTerm = null;
				}
			}

			result = ParseWord(word.ToString());

			while (groupStack.Count > 0)
				result = JoinExpressions(groupStack.Pop(), result);

			QueryOperation operation = result as QueryOperation;

			if (operation != null && operation.MissingOperand)
				result = operation.Normalize();

			if (optimize && result != null)
				return result.Optimize() ?? EmptyQueryExpression.Value;
			else
				return result ?? EmptyQueryExpression.Value;
		}
		#endregion
	}
}
